package kbs.scm
{
    import mx.rpc.http.HTTPService;
    import mx.rpc.events.ResultEvent;
    import mx.rpc.events.FaultEvent;
    import mx.modules.Module;
    import mx.managers.PopUpManager;

    import kbs.scmconsole.Console;
    import kbs.inspector.Inspect;
    import flash.display.DisplayObjectContainer;
    import flash.system.ApplicationDomain;
    import mx.controls.Alert;

    public class Eval extends Module
    {
        private var _ignore:Inspect;

        public function Eval()
        { m_global = new Env(null, this); }

        public function showConsole
            (p:DisplayObjectContainer):void
        {
            var console:Console = Console
                (PopUpManager.createPopUp(p, Console, false));
            console.m_evaluator = this;
            m_global.store
                ("workspace", new ObjectWrapped(p));
            m_global.store
                ("application-domain", new ObjectWrapped
                 (ApplicationDomain.currentDomain));
            m_global.store
                ("evaluator", new ObjectWrapped(this));
            PopUpManager.centerPopUp(console);

        }

        public function scmeval(exp:Base, env:Env):Base
        {
            try { return _scmeval(exp, env); }
            catch (e:Object) {
                scmalert(""+e);
                //scmalert(prettify(e));
            }
            return null;
        }

        private function _scmeval(exp:Base, env:Env):Base
        {
            if (exp is Primitive) {
                return exp;
            }
            else if (exp is Variable) {
                return env.lookup((exp as Variable).getName());
            }
            else if (exp is AList) {
                var a:AList = exp as AList;
                if (a.length() === 0) {
                    return a;
                }

                var f:Base = a.get(0);

                // special forms
                if (f is Define) {
                    // must be a 3 element list
                    if (a.length() !== 3) {
                        throw new Error("define -- must have 2 args "+a.length());
                    }
                    var vn:Variable = a.get(1) as Variable;
                    var vv:Base = scmeval(a.get(2), env);
                    env.store(vn.getName(), vv);
                    return vv;
                }
                if (f is Quote) {
                    // must be a 2 element list
                    if (a.length() !== 2) {
                        throw new Error("quote -- must have 1 arg "+a.length());
                    }
                    return a.get(1);
                }
                if (f is Lambda) {
                    // at least three elements
                    if (a.length() < 3) {
                        throw new Error("lambda -- must have at least 3 elements "+a.length());
                    }
                    // first element must be a list
                    if (!(a.get(1) is AList)) {
                        throw new Error("lambda -- need arguments as a list "+a.get(0));
                    }
                    return new LambdaFunction(a.get(1) as AList,
                                              a.nth(2), env);
                }

                // eval f
                var evalf:Base = scmeval(f, env);

                // eval args
                var evalargs:AList = new AList([]);
                for (var i:int=1; i<a.length(); i++) {
                    evalargs.append(scmeval(a.get(i), env));
                }

                // and apply
                return scmapply(evalf, evalargs, env);
            }
            else {
                throw new Error("Sorry -- unexpected type "+exp);
            }
        }

        public function scmapply(f:Base, al:AList, env:Env):Base
        {
            // Take care of primitive applications
            if (f is PrimitiveFunction) {
                return (f as PrimitiveFunction).scmapply(al, env);
            }
            else if (f is LambdaFunction) {
                var l:LambdaFunction = f as LambdaFunction;
                // Do the standard trick.
                // 1. Create a new environment based off the
                //    captured environment from the procedure
                var nenv:Env = new Env(l.getEnvironment(), this);
                // 2. Bind the named variables with the arguments
                var fl:AList = l.getArguments();
                if (al.length() != fl.length()) {
                    throw new Error("function needs "+fl.length()+" arguments, but I see only "+al.length()+" parameters");
                }
                for (var i:int =0; i<al.length(); i++) {
                    nenv.store((fl.get(i) as Variable).getName(), al.get(i));
                }
                // 3. evaluate the body in the new environment.
                var bdy:AList = l.getBody();
                var ret:Base = new AList([]);
                for (i =0; i<bdy.length(); i++) {
                    ret = scmeval(bdy.get(i), nenv);
                }
                return ret;
            }
            else {
                throw new Error("Sorry -- `"+f+"' is not a function");
            }
        }

        // show a stack trace if possible
        private function prettify(e:Error):String
        {
            var ret:String = e.toString();
            if (e.getStackTrace() != null) {
                ret += "\n\n"+e.getStackTrace();
            }
            return ret;
        }

        public function loadString(x:String, env:Env):Base
        {
            var re:RegExp = /\s*(\(|\)|[^\s()\"]+|\"[^\"]+\"|$)/g;
            var toks:Array = [];
            var a:Array;
            while (true) {
                a = re.exec(x);
                if ((a == null) || (a[1] === '')) {
                    break;
                }
                toks.push(a[1]);
            }
            // Now keep on evaluating the parsed tokens until
            // we reach hte last one.
            var idx:Object = {'idx' : 0};
            var ret:Base = null;
            while (idx.idx < toks.length) {
                ret = scmeval(scmparse_toks(toks, idx), env);
            }
            return ret;
        }

        // Get this remote file, and load/eval it in
        public function loadFile(path:String):Base
        {
            var s:HTTPService = new HTTPService();
            s.url = path;
            s.resultFormat = "text";
            s.addEventListener(ResultEvent.RESULT, function (ev:ResultEvent):void {
                    loadString(ev.result as String, m_global);
                });
            s.addEventListener(FaultEvent.FAULT, function (ev:FaultEvent):void {
                    scmalert("Failed to load `"+path+"': "+ev.message);
                });
            s.send();
            return null;
        }

        public function scmparse_toks(toks:Array, ptr:Object):Base
        {
            //scmdebug("idx = "+ptr.idx+" with token `"+toks[ptr.idx]+"'");
            var r:String = toks[ptr.idx];
            ptr.idx = ptr.idx+1;

            if (r.search(/^\".*\"$/) == 0) {
                //scmdebug("  is a string");
                return new Str(r.substring(1, r.length-1));
            }
            else if (r.search(/^[0-9]+$/) == 0) {
                //scmdebug("  is a number");
                return new Num(int(r));
            }
            else if (r.search(/^[^\$]+\.[^\.\$]+$/) == 0) {
                //scmdebug("  is a static method");
                var tmp:int = r.lastIndexOf('.');
                return new StaticMethodFunction(r.substring(0, tmp),
                                                r.substring(tmp+1,
                                                            r.length));
            }
            else if (r.search(/^[^\$]+\.$/) == 0) {
                //scmdebug("  is a newinstance");
                return new NewInstanceFunction(r.substring(0, r.length-1));
            }
            else if (r.search(/^\.[^\.\$]+$/) == 0) {
                //scmdebug("  is an instancemethod");
                return new InstanceMethodFunction
                    (r.substring(1, r.length));
            }
            else if (r.search(/^\.[^\.\$]+\$$/) == 0) {
                //scmdebug("  is an property");
                return new PropertyFunction
                    (r.substring(1, r.length-1));
            }
            else if (r === 'define') {
                return new Define();
            }
            else if (r === 'quote') {
                return new Quote();
            }
            else if (r === 'lambda') {
                return new Lambda();
            }
            else if (r.search(/^[^\$\.\(\)]+$/) == 0) {
                //scmdebug("  is a variable `"+r+"'");
                return new Variable(r);
            }
            else if (r === '(') {
                var ret:AList = new AList([]);
                //scmdebug("Pushing parse...");
                while ((ptr.idx < toks.length) &&
                       (toks[ptr.idx] !== ')')) {
                    ret.append(scmparse_toks(toks, ptr));
                }
                if (toks[ptr.idx] !== ')') {
                    throw new Error("Unexpected EOF");
                }
                ptr.idx = ptr.idx+1;
                return ret;
            }
            else {
                throw new Error("Sorry -- unexpected "+r);
            }
        }

        public function dump(x:Base):String
        {
            if (x === null) {
                return "nil";
            }
            return x.pprint();
        }
        public function scmdebug(s:String):void
        { trace(s); }

        public function scmalert(s:String):void
        { Alert.show(s); }

        public var m_global:Env;
    }
}
